#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os, os.path, sys
import time

from bsddb import db

import log

class   KeyExistError(Exception) : pass
class   KeyNotFoundError(Exception) : pass
class   AppendError(Exception) : pass
class   PickError(Exception) : pass
class   KeyConsistError(Exception) : pass

#######################################################################

class   KeySmith(object) :
    OP_INSERT = 0x00
    OP_ERASE  = 0xFF

    SEPARATOR = ' '

    def __init__(self) :
        self.keystring = None

        self.key = None
        self.timestamp = 0
        self.expire = 0
        self.operation = 0
        # __init__()

    def getTimestamp(self) :
        return int(time.time())
        # getTimestamp()

    def assemble(self, key, operation, expire=0, timestamp=None) :
        if not expire : expire = 0
        if expire :
            if not timestamp : timestamp = self.getTimestamp()
            expire += timestamp
        else :
            timestamp = 0

        self.keystring = self.SEPARATOR.join((key, '%X' % timestamp, '%02X' % operation, '%X' % expire))
        self.key       = key
        self.operation = operation
        self.expire    = expire
        self.timestamp = timestamp

        return self.keystring
        # assemble()

    def desemble(self, keystring) :
        fields = keystring.split(self.SEPARATOR)
        if len(fields) != 4 :
            raise RuntimeError, 'Invalid key string: %s.' % `keystring`

        self.key       = fields[0]
        self.timestamp = int(fields[1], 16)
        self.operation = int(fields[2], 16)
        self.expire    = int(fields[3], 16)

        self.keystring = keystring
        # desemble()

    # class Key

#######################################################################


class   Tuckahoe(object) :
    def __init__(self, dbpath, monolithic=True) :
        self.db_path = dbpath
        self.db_env = None

        self.db_type = db.DB_BTREE
        self.db_mode = 0660
        self.db_name = None
        self.db_open_flags = 0
        self.db_set_flags = 0

        self.monolithic = monolithic

        self.dbh = None
        self.__inited = False
        # __init__()

    def __del__(self) :
        self.__exit()
        # __del__()

    def __init(self) :
        if self.__inited : return True

        self.dbh = db.DB(self.db_env)
        self.dbh.set_flags(self.db_set_flags)
        self.dbh.set_pagesize(8192)     # default 4k, 10000records: 72s --> 27s
        self.dbh.set_bt_minkey(3)       # bad --> good :  4 << 3 < 2
        #self.dbh.set_pagesize(16384)     # default 4k, 10000records: 72s --> 18s
        #self.dbh.set_bt_minkey(8)        # bad --> good :  8 << 6 < 7 < 4 < 2
        if self.db_name:
            self.dbh.open(self.db_path, self.db_name, self.db_type,
                            self.db_open_flags|db.DB_CREATE, self.db_mode)
        else:
            self.dbh.open(self.db_path,
                            mode = self.db_mode,
                            dbtype = self.db_type,
                            flags = self.db_open_flags|db.DB_CREATE)

        self.__inited = True
        # __init()

    def __exit(self) :
        if not self.__inited : return

        self.dbh.sync()
        self.dbh.close()
        self.dbh = None
        # __exit()

    def insert(self, key, value)    :
        '''insert (key, value) pair

        if key-exists, just return
        else insert them
        '''
        if not self.__inited : self.__init()

        try:
            self.dbh.put(key, value, flags=db.DB_NOOVERWRITE)
        except db.DBKeyExistError, ex:
            #assert ex[0] == db.DB_KEYEXIST
            if log.debugable : log.debug('KeyExistError: %s', ex)
            raise KeyExistError, ex[1]
        except :
            log.excpt()
            raise
        # insert()

    def erase(self, key)    :
        '''erase record from DB by key

        if key exists, erase it
        else just return
        '''
        if not self.__inited : self.__init()

        try :
            self.dbh.delete(key)
        except db.DBNotFoundError, ex:
            #assert ex[0] == db.DB_NOTFOUND
            if log.debugable : log.debug('NotFoundError: %s', ex)
            pass
        except :
            log.excpt()
            raise
        # erase()

    def find(self, key) :
        '''find record from DB by key

        if key exists, return value
        else return None
        '''
        if not self.__inited : self.__init()

        try :
            value = self.dbh.get(key)
            return value
        except db.DBNotFoundError, ex:
            #assert ex[0] == db.DB_NOTFOUND
            if log.debugable : log.debug('NotFoundError: %s', ex)
            return None
        except :
            log.excpt()
            raise
        # find()

    # class Tuckahoe


class   Yam(object) :
    def __init__(self, path) :
        self.path = path
        self._file_serial = self.__nextSerialNumber()
        self._filename= '.'.join(('store', '%04X' % self._file_serial, 'data'))
        self._store_data_path = os.sep.join((self.path, self._filename))
        self._filer = file(self._store_data_path, 'a+')
        # __init__()

    def __del__(self) :
        if self._filer :
            self._filer.close()
            self._filer = None
        # __del__()roger

    def __nextSerialNumber(self) :
        return 0
        # __nextSerialNumber

    def append(self, record) :
        self._filer.write(record)
        # append()

    def tellend(self) :
        '''tell offset of file END

        return (fileno, offset)
        '''
        SEEK_END = 2
        self._filer.seek(0, SEEK_END)
        offset = self._filer.tell()
        return self._file_serial,offset
        # tell()

    def pick(self, fileno, offset, length) :
        if fileno != self._file_serial :
            log.error('Wrong fileno:%d (current:%d)', fileno, self._file_serial)
            raise RuntimeError, 'Wrong fileno:%d (current:%d)' % (fileno, self._file_serial)

        SEEK_SET = 0
        self._filer.seek(offset, SEEK_SET)
        data = self._filer.read(length)
        if len(data) != length :
            log.error('Invalid data length:%d (expect:%d)', len(data), length)
            return None
        return data
        # pick()

    # class Yam

#######################################################################


class   CellStore(object) :
    '''
    A CellStore is made up of index-files and data-files
    '''
    KEY_FIELD_SEPARATOR = ' '
    KEY_DATA_SEPARATOR  = '\t'

    def __init__(self, store_path) :
        self.store_path = store_path
        self.store_index_path = os.sep.join((store_path, 'store.idx'))

        self._indexer = Tuckahoe(self.store_index_path)
        self._filer = Yam(self.store_path)

        self._keysmith = KeySmith()
        # __init__()

    def __del__(self) :
        if self._indexer :
            del self._indexer
        if self._filer :
            del self._filer
        # __del__()

    def insert(self, key, data, timeout=0) :
        floc = self._indexer.find(key)
        if floc :
            # key already exists
            raise KeyExistError

        # key not exists, insert it
        self._keysmith.assemble(key, KeySmith.OP_INSERT, timeout)
        record = ''.join((self._keysmith.keystring, '\t',data,'\n'))
        length = len(record)
        fileno, offset = self._filer.tellend()
        floc = self.KEY_FIELD_SEPARATOR.join((str(fileno),str(offset),str(length)))
        try :
            self._indexer.insert(key, floc)
        except :
            if log.debugable : log.debug('fail to INSERT Key:%s', `key`)
            return False
        self._filer.append(record)
        return True
        # insert()

    def erase(self, key) :
        floc = self._indexer.find(key)
        if not floc :
            # key not exists
            raise KeyNotFoundError

        # key exists, erase it
        self._keysmith.assemble(key, KeySmith.OP_ERASE)
        data = ''
        record = ''.join((self._keysmith.keystring, '\t',data,'\n'))
        try :
            self._indexer.erase(key)
        except :
            if log.debugable : log.debug('fail to ERASE Key:%s', `key`)
            return False
        self._filer.append(record)
        return True
        # erase()

    def lookup(self, key) :
        '''
        return (data,expiration)
        '''
        floc = self._indexer.find(key)
        if not floc :
            # key not exists
            #return (None, 0)
            raise KeyNotFoundError, 'Key %s not found.' % `key`

        fileno,offset,length = [ int(x) for x in floc.split(self.KEY_FIELD_SEPARATOR) ]
        try :
            record = self._filer.pick(fileno, offset, length)
        except :
            raise PickError
        else :
            if not record :
                raise PickError
        keystring,data = record.strip().split(self.KEY_DATA_SEPARATOR, 1)
        self._keysmith.desemble(keystring)
        if key != self._keysmith.key :
            raise KeyConsistError, 'Key %s got, but %s expected.' % (`self._keysmith.key`, `key`)
        return  data,self._keysmith.expire
        # lookup()

    # class CellStore
